fix: fail fast on endpoint response stalls - #462
Conversation
|
MLCommons CLA bot All contributors have signed the MLCommons CLA ✍️ ✅ |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #462 +/- ##
=======================================
Coverage ? 80.81%
=======================================
Files ? 153
Lines ? 21043
Branches ? 0
=======================================
Hits ? 17005
Misses ? 4038
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
02eeec7 to
a5b80ac
Compare
arekay-nv
left a comment
There was a problem hiding this comment.
The opt-in liveness guard is useful, but this implementation adds avoidable work and state to the load generator's hot paths. The most serious issue is retaining every completed UUID in a second phase-lifetime container, which is an O(total requests) memory regression at the repository's 50k+ QPS target. The disabled path also performs work for every response, and low-concurrency workloads can create/cancel a watchdog task per request. Please keep the disabled path inert and use bounded active-request state plus a single event/deadline-driven watchdog.
a5b80ac to
6d6f42a
Compare
|
Thanks for the comments @viraatc and @arekay-nv . All issues resovled other than one. For the per transition watchdog, I choosed a different approche than your suggested ,so flagging it here. the guard is still armed on the 0→1 in-flight transition and retired on drain — but it's now a single Also measured arm+cancel per request, under the eager task factory this repo uses in production:
It also leaves the timer heap clear once a phase drains, which covers the "stale timer work" part of your comment. let me know if that make sense to you, Thanks a lot! |
viraatc
left a comment
There was a problem hiding this comment.
Review Council: three actionable findings.
arekay-nv
left a comment
There was a problem hiding this comment.
Thanks for putting this!
Can you add a test that verifies the behavior for one long stuck request while other requests are making progress. From my understanding, this will still wait till all the requests are completed except the stuck one (regardless of how long it stays stuck), and then the timer will trigger after the specified timeout. If that is the case (and correct me if i misunderstood), it seems like overkill to track progress.
Also, can you clarify the phase data being cleared on timeout is intentional.
Yes, that is intentional: this is a session-level liveness guard, not a per-request deadline. While other requests are making progress, the session is still live; once they drain, the remaining stuck request is the only in-flight work and the timeout fires. Tracking progress avoids falsely failing an active session. I’ll add a test for that scenario and clarify the docs. |
8184ecb to
2feebbb
Compare
Keep the guard disabled by default; recommend a 300-second or longer deadline when enabled. Preserve standard artifacts after a liveness failure and cover session-wide progress semantics.
2feebbb to
7024848
Compare
Summary
settings.timeouts.no_progress_timeout_splus a--no-progress-timeoutCLI aliasWhen this is useful
Use this for automated runs where a request can be accepted but the endpoint then becomes silent — for example, a TensorRT-LLM disaggregated executor or KV-transfer stall that never reaches the normal terminal-error path. It is engine-agnostic: it catches the same client-visible silent failure through vLLM, a frontend, or transport. Without it the benchmark blocks until its outer wall-time limit, because the phase drain waits on responses that never arrive and
drain_timeoutdefaults to unlimited.Disabled by default. It starts after work is issued, runs only while requests are in flight, and resets on an observed stream chunk or final result. It does not diagnose or restart the backend — it makes the benchmark fail with a clear error. For non-streaming endpoints, configure it above the full expected request latency. For TensorRT-LLM disaggregated serving the documented starting value is 300 s, matching the executor
hang_detection_timeout.Changes since the last review
Rebased onto latest main and moved the setting into the
Timeoutsmodel added by #409 (@viraatc).completed_uuidsretained every completed UUIDregister_skippedadds tombstones now, so it no longer grows O(total requests) alongsideuuid_to_indexloop.call_laterTimerHandle — no task, noEvent, no per-iterationwait_forcohortwordingissue()docs/config/DESIGN.mdholds the tuning detailMeasured cost when disabled (A/B against the base commit): +3.5 ns per response, +9.4 ns per request. With
stream_all_chunks: false(the default) the main process sees 2 messages per request, not one per token.Measured cost of the arming change under the production eager task factory: arm+cancel per request went from 2205 ns to 535 ns (4.1x).
Also fixed while here
A receiver transport error could overwrite an earlier
NoProgressError. A stalled endpoint often drops its connection too, so the real diagnosis was being masked by a generic "receiver failed" message. First error now wins, with a regression test.Not adopted
The suggestion to use one phase/session-lifetime watchdog driven by an activity event. The guard is still armed on the 0→1 in-flight transition and retired on drain — now with a
TimerHandlerather than a task. This keeps the timer heap clear once a phase drains, and measured 4.1x cheaper than the reviewed version. Happy to switch to a resident watchdog if that is preferred.Validation
pytest tests/unit/config/test_schema.py tests/unit/commands/test_benchmark.py tests/unit/load_generator/test_async_session.py— 425 passedpre-commit run --all-files— all hooks pass except mypy, which reports 3 pre-existingos.sched_*affinityerrors on macOS incpu_affinity.py/token_metrics.py; neither file is touched by this PR and the symbols exist on Linuxpython scripts/regenerate_templates.py --checkEndpoint made no response progress for 10.0s with 1 request(s) in flight, 10.002 s after the phase started against a 10 s deadline. The 10 s value is fault-injection coverage, not the deployment recommendation.Timer lifecycle is covered in both directions: the perf issue cap (
stop_current_phase) deliberately leaves the guard armed so a stall during drain is still caught, while drain completion,stop(), a phase change, and session teardown each retire it.